ooxx也可以說是很熱門的練習題,所以今天就用React 來寫一個ooxx的小遊戲吧!
初始化先設定一個長度為9的陣列來記錄玩家下的位置,這邊用了array.from({length}*9) 就會製造出一個長度為9的陣列,但因為我沒有另外寫callback function去設定 ,所以實際上這個陣列就會是 [undefined,undefined,…undefined]
const [record, setRecord] = useState(Array.from({ length: 9 }))
樣式的部分就寫在module.scss, 托flex的福 ,可以很快的解決九宮格css的部分,切完版之後,我需要抓到玩家點擊的是第幾個格子,所以我這樣寫,看玩家點擊哪一個格子就把那個格子index帶入,看起來一切都很合理…
<div onClick={handclick(index)} ></div>
然後畫面就掛掉了。
紅字錯誤顯示無限迴圈 ,我沒料到這樣寫handclick(index)就會馬上執行 ,因為vue這樣寫都沒事(喂,因為handclick function 改變了record的陣列,然後我的九宮格格子是依靠record陣列繪製出來的,再次render變成無窮迴圈,所以這邊記得要改成arrow function,就可以正常運作
接著列出贏家的畫線有幾種可能, 這邊用九宮格的位置來記錄,第一格index為0…最後一格為8
const arr = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]
];
設定玩家a為圈圈,用1來代表,玩家b則為叉叉,用-1來代表,由玩家a先開始,所以初始值設定為1,玩家a下完之後,如果沒有勝負,則輪到玩家b
const [player, setPlayer] = useState(1)
每當玩家點擊格子, 該格子就會依照是哪位玩家 ,就放入對應的數字 1 or -1 ,並且馬上比對剛剛列出八種勝出方式的陣列,並將格子內的ox轉換成對應的數字加總並且取絕對值 ,如果為3就是有人勝出了, 中斷迴圈開始準備畫線
ex 如果record 陣列這三個index (0,1,2)的數字加起來為3 or -3 就代表有人勝出了!(可以畫出最上排的水平線)
const caculation = () => {
let length = arr.length
for (let i = 0; i < length; i++) {
let total = 0
for (let k = 0; k < 3; k++) {
total += record[arr[i][k]]
}
if (Math.abs(total) === 3) {
getPos(arr[i])
break
}
}
}
用canvas來畫線,在一開始就已經建立好畫布,只是先隱藏起來,這邊用偷懶的方式, 設定一個x、y的陣列,大概抓個格子的中線位置,所以畫的線不會很精準,先取出獲勝的組合假設是1、4、7好了,抓陣列第一個數字為起點,陣列最後一個數字為終點,再利用除法和取餘數,拿到這兩個格子是位在第幾列的第幾行
const getPos = (arr) => {
let start = arr[0]
let end = arr[2]
let position = [50, 150, 250]
let obj = {
start: {
x: position[start % 3],
y: position[Math.floor(start / 3)],
},
end: {
x: position[end % 3],
y: position[Math.floor(end / 3)],
}
}
drawLine(obj)
}
const drawLine = (obj) => {
const canvas = canvasRef.current;
ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(obj.start.x, obj.start.y);
ctx.lineTo(obj.end.x, obj.end.y);
ctx.lineWidth = 5;
ctx.stroke();
console.log(player + 'player winner')
}
但是,這時又碰到一個小小的問題
利用useref來抓dom裡的canvas,殊不知一直抓不到 !! 一直是undefined,這時候推斷是雖然我即時將canvas更換為顯示的狀態,但是因為非同步的關係,下一行的canvasRef.current此時還是undefined狀態
一開始用了一個比較蠢的解法是 setTimeout(() => {const canvas = canvasRef.current;...})的確可以運作,但這是這樣寫用膝蓋想也知道不合理,後來查到比較正確的方式是在useEffect階段來取得useRef,不過也失敗了,為什麼?因為我的canvas預設是隱藏,所以useRef抓到的就會是null,問題好像越變越複雜,仔細想想,其實canvas也沒必要隱藏,只要可以讓使用者點擊到被canvas蓋住的格子就可以了,所以就在canvas身上設定css穿透屬性:pointer-events: none;就解決了!
完整程式碼
import React, { useState, useEffect, useRef } from 'react';
import style from './style/game.module.scss'
export default () => {
const arr = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8],
[2, 4, 6]
];
const [record, setRecord] = useState(Array.from({ length: 9 }))
const [player, setPlayer] = useState(1)
const canvasRef = useRef()
const caculation = () => {
let length = arr.length
for (let i = 0; i < length; i++) {
let total = 0
for (let k = 0; k < 3; k++) {
total += record[arr[i][k]]
}
if (Math.abs(total) === 3) {
getPos(arr[i])
break
}
}
}
const getPos = (arr) => {
let start = arr[0]
let end = arr[2]
let position = [50, 150, 250]
let obj = {
start: {
x: position[start % 3],
y: position[Math.floor(start / 3)],
},
end: {
x: position[end % 3],
y: position[Math.floor(end / 3)],
}
}
drawLine(obj)
}
const drawLine = (obj) => {
const canvas = canvasRef.current;
ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(obj.start.x, obj.start.y);
ctx.lineTo(obj.end.x, obj.end.y);
ctx.lineWidth = 5;
ctx.stroke();
console.log(player + 'player winner')
}
const handleClick = (event, index) => {
record[index] = player
setRecord(record)
caculation()
setPlayer(player * -1)
}
const resetGame = () => {
setRecord(Array.from({ length: 9 }))
setPlayer(1)
}
return (
<div className={style.container}>
<canvas width="300" height="300" className={style.myCanvas} ref={canvasRef} />
{
record.map((vo, k) => {
return (
<div className={style.item}
key={k}
onClick={(event) => handleClick(event, k)}>
<span className={style.content}>
{vo > 0 ? 'o' : vo < 0 ? 'x' : ''}
</span>
</div>
)
})
}
<button onClick={resetGame}>resetGame</button>
</div>
)
}
最終成品!